﻿using System.Collections.Generic;
using System.Linq.Expressions;

namespace System.Reflection
{
    public static class PropertyInfoExtension
    {
        private static class PropertiesCache<T>
        {
            private static readonly Dictionary<Type, Dictionary<string, Func<object, T>>> getters
                = new Dictionary<Type, Dictionary<string, Func<object, T>>>();

            private static readonly Dictionary<Type, Dictionary<string, Action<object, T>>> setters
                = new Dictionary<Type, Dictionary<string, Action<object, T>>>();

            public static T GetValue(object subj, string propName)
            {
                var tp = subj.GetType();
                return getters
                    .GetOrAddDefault(tp, () => new Dictionary<string, Func<object, T>>())
                    .GetOrAddDefault(propName, () => tp.GetProperty(propName).GetValueGetter<T>())(subj);
            }

            public static void SetValue(object subj, string propName, T value)
            {
                var tp = subj.GetType();
                setters
                    .GetOrAddDefault(tp, () => new Dictionary<string, Action<object, T>>())
                    .GetOrAddDefault(propName, () => tp.GetProperty(propName).GetValueSetter<T>())(subj, value);
            }
        }

        public static T GetValue<T>(object subj, string propName)
        {            
            return PropertiesCache<T>.GetValue(subj,propName);
        }

        public static void SetValue<T>(object subj, string propName, T value)
        {
            PropertiesCache<T>.SetValue(subj, propName, value);
        }

        public static Func<object, T> GetValueGetter<T>(this PropertyInfo propInfo)
        {
            var instance = Expression.Parameter(typeof(object), "i");
            var castedInstance = Expression.ConvertChecked(instance, propInfo.DeclaringType);
            var prop = Expression.Property(castedInstance, propInfo);
            var convert = Expression.Convert(prop, typeof(T));
            var expression = Expression.Lambda(convert, instance);
            return (Func<object, T>)expression.Compile();
        }

        public static Action<object, T> GetValueSetter<T>(this PropertyInfo propInfo)
        {
            var instance = Expression.Parameter(typeof(object), "i");
            var castedInstance = Expression.ConvertChecked(instance, propInfo.DeclaringType);
            var argument = Expression.Parameter(typeof(T), "a");
            var setterCall = Expression.Call(castedInstance, propInfo.GetSetMethod(), Expression.Convert(argument, propInfo.PropertyType));
            return (Action<object, T>)Expression.Lambda(setterCall, instance, argument).Compile();
        }
    }
}
